Back to Blog Tutorial

Building a Data Table: Using Only HTML Structure and SQL

Same Contacts data as last post, same idea of HTML structure and SQL — but this time the output is a real, structured <table>, not a grid of cards.

Apr 2005·5 min read·Post 4 of 8

A card grid is free-form — each row can contain whatever layout you like. A table is not: it has columns, a header row, and every row needs to line up under the same headings. That structural difference is exactly why the engine gives it a separate element, dynamiclisttable, rather than making you fake a table out of a styled dynamiclist.

1 The XML

Same Contacts table, same tenant scoping, this time with a department column added to make the tabular shape worth showing:

emxml_blkContactsTable.xml
<Block tag='div'>

    <Element type='dynamiclisttable'>
    <Attributes class='contacts-table' />

    <TableHeader>
        <Element type='html'>
            <Content><![CDATA[
                <tr>
                    <th>Name</th>
                    <th>Department</th>
                    <th>Email</th>
                    <th class='num'>Open Orders</th>
                </tr>
            ]]></Content>
        </Element>
    </TableHeader>

    <TableRow>
        <Element type='html'>
            <Content><![CDATA[
                <td class='name'>%contact_name%</td>
                <td><span class='dept-pill'>%department%</span></td>
                <td>%email%</td>
                <td class='num'>%open_order_count%</td>
            ]]></Content>
        </Element>
    </TableRow>

    <!-- one query, SQL last, same rule as dynamiclist -->
    <SQL>
        <query name='contacts_table'>
            <![CDATA[
                SELECT c.contact_id, c.contact_name, c.department, c.email,
                       COUNT(o.order_id) AS open_order_count
                FROM   contacts c
                LEFT JOIN orders o ON o.contact_id = c.contact_id AND o.status = 'open'
                WHERE  c.dept_id = '%deptid%'
                GROUP BY c.contact_id
                ORDER BY c.contact_name ASC
            ]]>
        </query>
    </SQL>
    </Element>

</Block>

What's different from a dynamiclist

<Block> wrapper
Same rule as post 3's list — the file's root is <Block>, with the dynamiclisttable nested one level in as its child, not as the root itself.
TableHeader
Renders once as the <thead> row. A dynamiclist has no equivalent — a card grid doesn't have column headings to keep aligned.
TableRow
The per-row <tbody> template — same repeat-per-result-row idea as ListRow, just constrained to <td> cells that line up under the header.
no manual <table>
The engine generates the <table>, <thead>, and <tbody> wrapper tags itself. Opening or closing them by hand across separate elements breaks the structure — the whole table has to come from this one element.
one query only
Same rule as dynamiclist: exactly one <query> per element. Need data from more than one table? JOIN it into that single query, the way open_order_count is joined in above, rather than adding a second query.
2 Generated HTML
Generated HTML
<table class="contacts-table">
    <thead>
        <tr>
            <th>Name</th><th>Department</th><th>Email</th><th class="num">Open Orders</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="name">Sarah Whitfield</td>
            <td><span class="dept-pill">Operations</span></td>
            <td>s.whitfield@example.com</td>
            <td class="num">3</td>
        </tr>
        <!-- repeated once per row -->
    </tbody>
</table>
3 Rendered live
app.example.com/manager/contacts?view=table
NameDepartmentEmailOpen Orders
Sarah WhitfieldOperationss.whitfield@example.com3
David OkonkwoProcurementd.okonkwo@example.com7
Maria SantosFinancem.santos@example.com0
James CooperWarehousej.cooper@example.com2
The open_order_count column comes from a JOIN and a COUNT in the same single query — not a second round trip, not a per-row lookup. One query, every column.

When to reach for which

Neither component is "better" — dynamiclisttable is purpose-built for column-based data: it generates the header row and keeps every column aligned underneath it automatically. dynamiclist is the more versatile of the two, capable of far more than card-style rows — including composing with dynamiclisttable in ways worth their own dedicated look. Post 3's grid and this post's table are rendering the same underlying contacts — dynamiclisttable just meant not having to hand-build the header row and column alignment ourselves.